﻿/*
    Copyright 2010, 2011 Eric Wong
	contact@optimalcycling.com
	http://www.optimalcycling.com
	Optimal Cycling - Advanced power pacing program for cyclists
	
	This file is part of Optimal Cycling.

    Optimal Cycling is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Optimal Cycling is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Optimal Cycling.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.IO;
using System.ComponentModel;

namespace OptimalCycling.FileIO.OC
{
    /// <summary>
    /// Performs file I/O for the Optimal Cycling V2.1 file format
    /// </summary>
    public class FileOCDataV2_1
    {
        public const int CONSOLE_OUT_MAX_SIZE = 1024000;
        public const int CONSOLE_OUT_DROP_SIZE = CONSOLE_OUT_MAX_SIZE / 2;

        /// <summary>
        /// Reads the files to a DataSet.
        /// </summary>
        /// <param name="ocData">Data read from the data files is stored in this DataSet for easy access.</param>
        /// <param name="folderPath">Full file path to the folder containing the data (.csv files) to be read.</param>
        /// <returns>True if the read data was converted from an older format or invalid data was corrected.</returns>
        public static bool ReadOCData(out DataSet ocData, String folderPath)
        {
            ocData = null;
            
            if (!Directory.Exists(folderPath))
                throw new Exception("Folder " + folderPath + " does not exist!");

            if (folderPath[folderPath.Length - 1] != Path.DirectorySeparatorChar)
                folderPath = folderPath + Path.DirectorySeparatorChar;

            CreateBlankTemplate(out ocData, false);
            ocData.DataSetName = folderPath;

            #region Input Data
            OptimalCyclingFileFormat fileFormat = OptimalCyclingFileFormat.Unknown;

            for (int i = 0; i < DataFiles.tableGroupInfo.Length; i++)
            {
                for (int j = 0; j < DataFiles.tableGroupInfo[i].TableInfo.Length; j++)
                {                    
                    String filePath = Path.Combine(folderPath, DataFiles.tableGroupInfo[i].TableInfo[j].TableName);
                    DataTable destinationTable = ocData.Tables[DataFiles.tableGroupInfo[i].TableInfo[j].TableName];

                    if (!File.Exists(filePath))
                    {
                        destinationTable.BeginLoadData();
                        DataRow dr = destinationTable.NewRow();
                        destinationTable.Rows.Add(dr);
                        destinationTable.EndLoadData();
                        continue;
                    }

                    // Enable reading of already open files
                    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, System.IO.FileShare.ReadWrite);

                    StreamReader readTable = new StreamReader(fs);
                    DataTable tempTable = CsvParser.Parse(readTable, true);
                    readTable.Close();
                    tempTable.TableName = DataFiles.tableGroupInfo[i].TableInfo[j].TableName;

                    // Convert files (tempTable) from older formats here
                    if (fileFormat == OptimalCyclingFileFormat.OptimalCyclingDataV2_0)
                    {
                        if (tempTable.TableName == DataFiles.course.TableName && !tempTable.Columns.Contains(CourseColNames.generalCdA.ColName))
                        {
                            tempTable.Columns[@"General CdA [unitless]"].ColumnName  = CourseColNames.generalCdA.ColName;
                        }
                    }

                    destinationTable.BeginLoadData();

                    for (int k = 0; k < tempTable.Rows.Count && k < DataFiles.tableGroupInfo[i].TableInfo[j].MaxRows; k++)
                    {
                        DataRow dr = destinationTable.NewRow();
                        for (int m = 0; m < tempTable.Columns.Count; m++)
                        {
                            Object pieceOfData = tempTable.Rows[k][m];
                            if (destinationTable.Columns.Contains(tempTable.Columns[m].ColumnName))
                            {
                                if (pieceOfData == DBNull.Value || pieceOfData.ToString().Length <= 0)
                                {
                                    //dr[k] = DBNull.Value;
                                }
                                else
                                {
                                    // try...catch is very time consuming
                                    try
                                    {
                                        dr[tempTable.Columns[m].ColumnName] = pieceOfData;
                                    }
                                    catch (Exception)
                                    {
                                        //dr[k] = DBNull.Value;
                                    }
                                }
                            }
                        }
                        destinationTable.Rows.Add(dr);
                    }

                    if (destinationTable.Rows.Count <= 0 && DataFiles.tableGroupInfo[i].TableInfo[j].MaxRows >= 1)
                    {
                        DataRow dr = destinationTable.NewRow();
                        destinationTable.Rows.Add(dr);
                    }

                    // Verify the ranges of the data values in this data table
                    for (int k = 0; k < destinationTable.Rows.Count; k++)
                    {
                        for (int m = 0; m < destinationTable.Columns.Count; m++)
                        {
                            if (!ValidRange(DataFiles.tableGroupInfo[i].TableInfo[j].DataColInfo[m], destinationTable.Rows[k][m]))
                                throw new Exception("Data out of range at table " + destinationTable.TableName + ", row " + k + ", column " + m + ".");
                        }
                    }

                    destinationTable.EndLoadData();


                    if (i == 0 && j == 0)   // First file read is always FileInformation.csv
                    {
                        fileFormat = (OptimalCyclingFileFormat)Enum.Parse(typeof(OptimalCyclingFileFormat),
                                        ocData.Tables[DataFiles.fileInformation.TableName].Rows[0][FileInfoColNames.fileVersion.ColName].ToString(), true);

                        if (fileFormat == OptimalCyclingFileFormat.OptimalCyclingDataV2_0)
                        {                            
                            destinationTable.BeginLoadData();
                            ocData.Tables[DataFiles.fileInformation.TableName].Rows[0][FileInfoColNames.fileVersion.ColName] = OptimalCyclingFileFormat.OptimalCyclingDataV2_1.ToString();
                            destinationTable.EndLoadData();
                        }
                        else if (fileFormat != OptimalCyclingFileFormat.OptimalCyclingDataV2_1)
                            throw new Exception("This file format is invalid/not supported by this version of Optimal Cycling. Try upgrading.");
                    }
                }
            }
            ocData.AcceptChanges();
            #endregion

            if (fileFormat != OptimalCyclingFileFormat.OptimalCyclingDataV2_1)
                return true;
            else
                return false;
        }

        /// <summary>
        /// Test to see if a value is in the wanted range.
        /// </summary>
        /// <param name="dataColInfo">Contains the valid range we are testing for.</param>
        /// <param name="data">The value to be tested. Must be either a double, float, int, uint, long, or ulong type.</param>
        /// <returns>True if "data" is in the range specified by "dataColInfo", or data is not of the types double, float, int, uint, long, or ulong. False otherwise.</returns>
        public static bool ValidRange(DataColInfo dataColInfo, object data)
        {
            if (dataColInfo.LowLimit != null)
            {
                if (dataColInfo.DataType == typeof(double) || dataColInfo.DataType == typeof(float) || dataColInfo.DataType == typeof(int)
                    || dataColInfo.DataType == typeof(uint) || dataColInfo.DataType == typeof(long) || dataColInfo.DataType == typeof(ulong))
                {
                    double value = Convert.ToDouble(data);
                    double lowLimit = Convert.ToDouble(dataColInfo.LowLimit);
                    if (value < lowLimit) return false;
                }
            }
            else if (dataColInfo.HighLimit != null)
            {
                if (dataColInfo.DataType == typeof(double) || dataColInfo.DataType == typeof(float) || dataColInfo.DataType == typeof(int)
                    || dataColInfo.DataType == typeof(uint) || dataColInfo.DataType == typeof(long) || dataColInfo.DataType == typeof(ulong))
                {
                    double value = Convert.ToDouble(data);
                    double highLimit = Convert.ToDouble(dataColInfo.HighLimit);
                    if (value < highLimit) return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Writes the data to file.
        /// </summary>
        /// <param name="ocData">The data to be written.</param>
        /// <param name="writeResultsOnly">If true, write only the program results instead of everything.</param>
        /// <param name="folderPath">The folder where to write the files to.</param>
        /// <returns>N/A</returns>
        public static void WriteOCData(DataSet ocData, bool writeResultsOnly, String folderPath)
        {
            ocData.AcceptChanges();

            if (!Directory.Exists(folderPath))
                Directory.CreateDirectory(folderPath);

            if (folderPath[folderPath.Length - 1] != Path.DirectorySeparatorChar)
                folderPath = folderPath + Path.DirectorySeparatorChar;

            string intermedFolderPath = Path.Combine(folderPath, MiscFiles.intermedFolderName);
            if (!Directory.Exists(intermedFolderPath))
            {
                Directory.CreateDirectory(intermedFolderPath);
            }

            if (writeResultsOnly)
            {
                for (int i = 0; i < DataFiles.tableInfoResults.Length; i++)
                {
                    DataTable dataToWrite = ocData.Tables[DataFiles.tableInfoResults[i].TableName];
                    String filePath = folderPath + dataToWrite.TableName;

                    // Enable writing of already open files
                    FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, System.IO.FileShare.ReadWrite);
                    StreamWriter writeTable = new StreamWriter(fs);
                    CsvWriter.WriteToStream(writeTable, dataToWrite, true, false);
                    writeTable.Flush();
                    writeTable.Close();
                }
            }
            else
            {
                for (int i = ocData.Tables.Count - 1; i >= 0; i--) // Write in reverse order to ensure FileInformation.csv is written last
                {
                    DataTable dataToWrite = ocData.Tables[i];
                    String filePath = folderPath + dataToWrite.TableName;

                    // Enable writing of already open files
                    FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, System.IO.FileShare.ReadWrite);
                    StreamWriter writeTable = new StreamWriter(fs);
                    CsvWriter.WriteToStream(writeTable, dataToWrite, true, false);
                    writeTable.Flush();
                    writeTable.Close();
                }
            }

            string consoleOutPath = Path.Combine(folderPath, MiscFiles.consoleFileName);
            if (!File.Exists(consoleOutPath))
            {
                StreamWriter consoleOutFileWriter = new StreamWriter(consoleOutPath);
                consoleOutFileWriter.Write("");
                consoleOutFileWriter.Flush();
                consoleOutFileWriter.Close();
            }
        }

        /// <summary>
        /// Creates a blank template (DataSet) to store the simulation data.
        /// </summary>
        /// <param name="ocData">The template is stored in this DataSet.</param>
        /// <param name="prefill">If true, prefill the template with default values for the columns instead of being blank.</param>
        /// <returns>N/A</returns>
        public static void CreateBlankTemplate(out DataSet ocData, bool prefill)
        {
            ocData = new DataSet(FileIOConstants.DATA_FILES);

            for (int i = 0; i < DataFiles.tableGroupInfo.Length; i++)
            {
                for (int j = 0; j < DataFiles.tableGroupInfo[i].TableInfo.Length; j++)
                {
                    DataTable newTable = new DataTable(DataFiles.tableGroupInfo[i].TableInfo[j].TableName);

                    // Create each of the columns in this newTable
                    for (int k = 0; k < DataFiles.tableGroupInfo[i].TableInfo[j].DataColInfo.Length; k++)
                    {
                        newTable.Columns.Add(DataFiles.tableGroupInfo[i].TableInfo[j].DataColInfo[k].ColName,
                            DataFiles.tableGroupInfo[i].TableInfo[j].DataColInfo[k].DataType);
                        newTable.Columns[k].DefaultValue = DataFiles.tableGroupInfo[i].TableInfo[j].DataColInfo[k].DefaultVal;
                    }

                    // Load the table with initial data
                    if (prefill && DataFiles.tableGroupInfo[i].TableInfo[j].MaxRows >= 1)
                    {
                        newTable.BeginLoadData();
                        DataRow newRow = newTable.NewRow();
                        newTable.Rows.Add(newRow);
                        newTable.EndLoadData();
                    }

                    ocData.Tables.Add(newTable);
                }
            }
            ocData.AcceptChanges();
        }
    }
}
